// 代码编辑器中的TextLayout认为style不会影响字符的宽高和断行.
// 为了方便计算, 只支持等宽字体, 字符分为普通字符和宽字符与零宽字符.
// 暂不支持wordWrap.

function TextLayout(model){
    this.$initEventable();
    this._model = model;
    this.$on(this._model, 'Changed', this._onModelChanged);
    this._lineTracker = new LineTracker(this._model);
    this._tabSize = 4;
    this._charWidth = 8;
    this._fullCharWidth = 15;
    this._lineHeight = 20;
    this._wrapLimit = 80;
    this._useWordWrap = false;
    this._allRuns = null;
    // 用于快速的从offset映射到Run, todo
    this._quickRunsOffset = [];
    this._wrapData = [];
    this._width = 0;
    this._height = 0;
    this._displayLineEndStops = null;
    this._spaceChar = '&#160;';
    this._fwCharsStartTag = '<span>';
    this.$computeAll();
}

var _regex = /([\t]+)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]+)|[\v\f\uFEFF\u200B\u200C\u200D]+/g;
var _KIND_ZEROWIDTHCHARS = 0;
var _KIND_CHARS = 1;
var _KIND_FULLWIDTHCHARS = 2;
var _KIND_TAB = 4;
var _KIND_FOLLOWTAB = 5;

TextLayout.prototype = {
    getWidth: function (){
        return this._width;
    },
    getHeight: function (){
        return this._height;
    },
    setTabSize: function (size){
        this._tabSize = size;
    },
    setWrapLimit: function (limit){
        this._wrapLimit = limit;
    },
    setCharWidth: function (width){
        this._charWidth = width;
    },
    // FIXME: 可能cjk字体的宽度并不统一.
    setFullCharWidth: function (width){
        this._fullCharWidth = width;
    },
    setLineHeight: function (height){
        this._lineHeight = height;
    },
    getLineCount: function (index){
        return this._lineTracker.getLineCount(index);
    },
    getLineIndex: function (offset){
        return this._lineTracker.getLineIndex(offset);
    },
    getLineStartOffset: function (index){
        return this._lineTracker.getLineStartOffset(index);
    },
    getLineEndOffset: function (index){
        return this._lineTracker.getLineEndOffset(index);
    },
    getLineText: function (index){
        return this._model.getText(this.getLineStartOffset(index), this.getLineEndOffset(index));
    },
    getLineDelimiter: function (index){
        return this._lineTracker.getLineDelimiter(index);
    },
    getDisplayLineCount: function (index){
        if (!this._useWordWrap) {
            return this.getLineCount(index);
        }
    },
    getDisplayLineIndex: function (offset){
        if (!this._useWordWrap) {
            return this.getLineIndex(offset);
        }
    },
    getDisplayLineStartOffset: function (index){
        if (!this._useWordWrap) {
            return this.getLineStartOffset(index);
        }
    },
    getDisplayLineEndOffset: function (index){
        if (!this._useWordWrap) {
            return this.getLineEndOffset(index);
        }
    },
    getDisplayLineText: function (index){
        return this._model.getText(this.getDisplayLineStartOffset(index), this.getDisplayLineEndOffset(index));
    },
    getOffsetAtPoint: function (point){
        var pointStop = point[0] / this._lineHeight;
        var unitWidth = this._charWidth;
        var displayLineIndex = this.getDisplayLineIndex(iOffset);
        var displayLineStartOffset = this.getDisplayLineStartOffset(displayLineIndex);
        var displayLineEndOffset = this.getDisplayLineEndOffset(displayLineIndex);
        if (pointStop <= 0) {
            return displayLineStartOffset;
        } else if (pointStop >= this._displayLineEndStops[displayLineIndex]) {
            return displayLineEndOffset;
        }
        var runIndexes = this._runIndexesOf(displayLineStartOffset);
        var allRuns = this._allRuns;
        var runIndex = runIndexes[0];

        var run = allRuns[runIndex];
        run = [run[0], run[1] - runIndexes[1]];

//        Utils.assert(stop != null);
        var stop = this.$getRunStop(run, 0);
        var runStop;
        var iOffset = displayLineStartOffset + run[1];
        if (stop === pointStop) {
            return iOffset;
        }
        if (stop < pointStop) {
            for (runIndex++; runIndex<allRuns.length; runIndex++) {
                run = allRuns[runIndex];
                runStop = this.$getRunStop(run, stop);
                if (stop + runStop === pointStop) {
                    return iOffset + run[1];
                } else if (stop + runStop > pointStop) {
                    break;
                }
                stop += runStop;
                iOffset += run[1];
            }
        }
        if (run[0] === _KIND_TAB) {
            runStop = this.$getRunStop(run, stop);
            if (pointStop < (stop + runStop) / 2) {
                return iOffset;
            } else {
                return iOffset + 1;
            }
        }
        var unit = run[0] === _KIND_CHARS ? 1 :
            run[0] === _KIND_FULLWIDTHCHARS ? 2 :
            run[0] === _KIND_FOLLOWTAB ? 4 : 0;
        return iOffset + Math.round((pointStop - stop) / unit);
    },
    getPointAtOffset: function (offset){
        var displayLineIndex = this.getDisplayLineIndex(offset);
        var displayLineStartOffset = this.getDisplayLineStartOffset(displayLineIndex);
        var runs = this.$getInlineRuns(displayLineStartOffset, offset);
        var stop = this.$getRunsStop(runs);
        return [stop * this._charWidth, displayLineIndex * this._lineHeight];
    },
    getPoint: function (displayLineIndex, stop){
        
    },
    $computeAll: function (){
        this.$computeRuns();
        if (this._useWordWrap) {
            this.$computeWrap();
        }
        this.$computeSize();
    },
    $computeRuns: function (){
        var m;
        var runs = [];
        var text = this._model.getText();
        var lastIndex = 0;
        _regex.lastIndex = 0;
        while (m = _regex.exec(text)) {
            var index = m.index;
            if (index > lastIndex) {
                runs.push([_KIND_CHARS, index - lastIndex]);
            }
            lastIndex = _regex.lastIndex;
            if (m[1]) {
                var tabs = m[0];
                runs.push([_KIND_TAB, 1 ]);
                if (tabs.length > 1) {
                    runs.push([_KIND_FOLLOWTAB, tabs.length - 1]);
                }
            } else {
                runs.push([m[2] ? _KIND_FULLWIDTHCHARS : _KIND_ZEROWIDTHCHARS, m[0].length ]);
            }
        }
        if (_regex.lastIndex !== text.length) {
            runs.push([_KIND_CHARS, text.length - _regex.lastIndex]);
        }
        if (!runs.length) {
            runs.push([_KIND_ZEROWIDTHCHARS, 0]);
        }
        this._allRuns = runs;
    },
    $computeWrap: function (){
        // todo: 更新_wrapData数组
        this._wrapData = [];
    },
    $updateLineWrap: function (line){

    },
    // todo: 这个函数改下名，并专门做stopcache方面的事情
    $computeSize: function (){
        var lineCount = this.getDisplayLineCount();
        var height = lineCount * this._lineHeight;
        var stops = [0];
        for (var i=0; i<lineCount; i++) {
            stops.push(this.$getStopAtOffset(this.getDisplayLineEndOffset(i)));
        }
        var maxStop = Math.max.apply(Math, stops);
        var width = maxStop * this._charWidth;
        this._displayLineEndStops = stops;
        var deltaPx = this._charWidth * 2 - this._fullCharWidth;
        this._fwCharsStartTag = ['<span style="letter-spacing:',
            deltaPx, 'px;padding-left:', deltaPx >> 1, 'px;">'].join('');
        
        if (this._width != width || this._height != height) {
            this._width = width;
            this._height = height;
            this.dispatchEvent({
                type: 'SizeChanged',
                width: width,
                height: height
            });
        }
    },
    $getRunStop: function (run, stop){
        stop = stop | 0;
        var tabSize = this._tabSize;
        if (run[0] == _KIND_CHARS) {
            stop = run[1];
        } else if (run[0] == _KIND_FULLWIDTHCHARS) {
            stop = run[1] * 2;
        } else if (run[0] == _KIND_TAB) {
            stop = tabSize - stop % tabSize;
        } else if (run[0] == _KIND_FOLLOWTAB) {
            stop = tabSize * run[1];
        }
        return stop;
    },
    $getRunsStop: function (runs, stop){
        var iStop = stop == null ? runs.stop || 0 : stop;
        for (var i=0; i<runs.length; i++) {
            iStop += this.$getRunStop(runs[i], iStop);
        }
        return iStop - stop;
    },
    _runIndexesOf: function (offset){
        var allRuns = this._allRuns;
        var i, run;
        var iOffset = 0;
        for (i=0; i<allRuns.length; i++) {
            run = allRuns[i];
            if (iOffset + run[1] >= offset) {
                return [i, offset - iOffset];
            }
            iOffset += run[1];
        }
        return [-1];
    },
    $getOffsetAtStop: function (displayLineIndex, stop, leftOrRight){
        
    },
    $getStopAtOffset: function (offset){
        var displayLineIndex = this.getDisplayLineIndex(offset);
        var displayLineStartOffset = this.getDisplayLineStartOffset(displayLineIndex);
        var runIndexes = this._runIndexesOf(displayLineStartOffset);
        var allRuns = this._allRuns;
        var runIndex = runIndexes[0];
        
        var run = allRuns[runIndex];
        run = [run[0], run[1] - runIndexes[1]];
        if (displayLineStartOffset + run[1] >= offset) {
            return this.$getRunStop([run[0], offset - displayLineStartOffset], 0);
        }
        
        var stop = this.$getRunStop(run);
        var iOffset = displayLineStartOffset + run[1];
        for (runIndex ++; runIndex<allRuns.length; runIndex++) {
            run = allRuns[runIndex];
            if (iOffset + run[1] >= offset) {
                return stop + this.$getRunStop([run[0], offset - iOffset], stop);
            }
            iOffset += run[1];
            stop += this.$getRunStop(run, stop);
        }
        Utils.assert(false);
    },
    $getInlineRuns: function (startOffset, endOffset){
        var allRuns = this._allRuns;
        var iOffset = 0;
        var runs = [];
        var i, run;
        for (i=0; i<allRuns.length; i++) {
            run = allRuns[i];
            if (iOffset + run[1] > startOffset) {
                run = [run[0], iOffset + run[1] - startOffset];
                iOffset = startOffset;
                break;
            }
            iOffset += run[1];
        }
        if (iOffset + run[1] == endOffset) {
            return [run[0], endOffset - iOffset];
        }
        runs.push(run);
        iOffset += run[1];
//        //如何注释这个断言?
//        Utils.assert(runs.length);
        for (i++; i<allRuns.length; i++) {
            run = allRuns[i];
            if (iOffset + run[1] >= endOffset) {
                runs.push([run[0], endOffset - iOffset]);
                break;
            }
            runs.push(run);
            iOffset += run[1];
        }
        return runs;
    },
    $renderInlineHTML: function (startOffset, endOffset){
        var runs = this.$getInlineRuns(startOffset, endOffset);
        var stop = this.$getStopAtOffset(startOffset);
        var buff = [];
        var lastOffset = startOffset;
        for (var i=0; i<runs.length; i++) {
            var run = runs[i];
            if (run[0] === _KIND_CHARS) {
                buff.push(this._model.getText(lastOffset, lastOffset + run[1]));
            } else if (run[0] === _KIND_FULLWIDTHCHARS) {
                buff.push(this._fwCharsStartTag, this._model.getText(lastOffset, lastOffset + run[1]), '</span>');
            } else if (run[0]) {
                var runStop = this.$getRunStop(run, stop);
                buff.push(Utils.stringRepeat(this._spaceChar, runStop));
            }
            lastOffset += run[1];
            stop += this.$getRunStop(run, stop);
        }
        return buff.join('');
    },
    _onModelChanged: function (){
        // todo: 不要更新全部，而是局部更新, 并发出事件告诉view哪些displayLine发生改变.
        this.$computeAll();
    }
};

Utils.extend(TextLayout, Eventable);